Skip to content

feat(domains): add forge activation methods - #320

Open
lucasfcosta wants to merge 2 commits into
mainfrom
feature/dev-2472-python-sdk-internal-squad-devtools
Open

lucasfcosta wants to merge 2 commits into
mainfrom
feature/dev-2472-python-sdk-internal-squad-devtools

Conversation

@lucasfcosta

@lucasfcosta lucasfcosta commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Forge activation to Domains, matching the folded API surface (the standalone forge verify and update endpoints no longer exist). Ports the Node implementation (resend/resend-node#1193, spec in resend/resend-openapi#144) into this SDK's idioms. Part of DEV-2472.

  • resend.Domains.Forge.enable({ "id": ..., "return_path": ... }) → POST /domains/{id}/forge. return_path is optional; when omitted, the API picks a label that is free in the domain's DNS. Returns the full domain response and is idempotent on the API side. Ships with its _async variant, like the rest of the SDK.
  • resend.Domains.verify(domain_id, params={ "scope": ["forge"] }) → POST /domains/{id}/verify. params is a new optional argument (Domains.VerifyParams, scope: List[Literal["forge"]]); scoped, only the Forge record is re-checked, so the records that are already verified are not re-proven. Without params the request is byte-identical to before. Same on verify_async.
  • resend.Domains.update({ "id": ..., "forge_return_path": ... }) → PATCH /domains/{id}. forge_return_path is a new UpdateParams key: moves the Forge return path while the Forge record is still verifying, does not start verification, and once the record verifies the API refuses with 422 return_path_unavailable. Composes with the other update fields.

Domain response types are unchanged: Forge state is read from the Forge CNAME record in the records array.

Validation

flake8 clean, mypy clean on the touched files, pytest: 875 tests passing. Tests assert the wire shapes: enable's empty body, verify's {"scope": ["forge"]} body (and {} without params), and update's forge_return_path, sync and async. examples/domain_forge.py now walks the folded flow: enable, move the path with Domains.update, verify scoped to forge.

Notes for reviewers

  • VerifyParams is a nested TypedDict on Domains with scope: NotRequired[List[VerifyDomainScope]], where VerifyDomainScope = Literal["forge"] lives next to TlsOptions.
  • The underlying class is DomainForge exposed as Domains.Forge (now enable-only), matching how DomainClaims is exposed as Domains.Claims.
  • Forge enable still strips id from the request body (it only addresses the path), following the ApiKeys.update precedent.

Summary by cubic

Adds a Forge subresource to Domains and folds Forge verification and return-path moves into the classic domain endpoints, per DEV-2472.

  • Domains.Forge.enable posts to /domains/{id}/forge with an optional return_path; id is stripped from the request body.
  • Domains.verify now accepts an optional scope: ["forge"] to re-check only the Forge record, and Domains.update accepts forge_return_path to move the return path without starting verification.
  • Bumps the SDK version to 2.54.0 and adds sync and async unit tests plus an examples/domain_forge.py example.

Validation

  • Unit tests cover URL, HTTP verb, body mapping, the empty-body enable, and the no-content error path for the new methods, sync and async.

Written for commit 242e609. Summary will update on new commits.

View guided diff

The Forge enable endpoint is live in production; the scoped verify and forge_return_path land with the API reshape, which deploys before this ships. Ships with the next resend-python release.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 9 files

Confidence score: 3/5

  • Running examples/domain_forge.py can change a live domain’s Return-Path and trigger re-verification, with no cleanup; the later update also raises once verification completes. Use a disposable domain and clean up the configuration.
  • tests/domain_forge_async_test.py checks only returned JSON, so incorrect URLs or HTTP verbs in the async methods can go unnoticed. Assert the mock call arguments, including the URL.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="examples/domain_forge.py">

<violation number="1" location="examples/domain_forge.py:8">
P2: This example changes a hardcoded live domain's Return-Path and triggers re-verification, with no cleanup. Running it can alter production sending config. Once the domain verifies, the `update` call raises the documented 422 `return_path_unavailable`, so the script cannot be rerun. Create a throwaway domain as `examples/domains.py` does, or read the ID from an environment variable.</violation>
</file>

<file name="tests/domain_forge_async_test.py">

<violation number="1" location="tests/domain_forge_async_test.py:23">
P3: The async Forge tests only check the returned JSON, so a wrong path or HTTP verb in `enable_async`, `update_async`, or `verify_async` would still pass. Add a `self.mock.call_args.kwargs["url"]` assertion, as the sync `domain_forge_test.py` and other async tests do.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread examples/domain_forge.py
if not os.environ["RESEND_API_KEY"]:
raise EnvironmentError("RESEND_API_KEY is missing")

domain_id = "d91cd9bd-1176-453e-8fc1-35364d380206"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This example changes a hardcoded live domain's Return-Path and triggers re-verification, with no cleanup. Running it can alter production sending config. Once the domain verifies, the update call raises the documented 422 return_path_unavailable, so the script cannot be rerun. Create a throwaway domain as examples/domains.py does, or read the ID from an environment variable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At examples/domain_forge.py, line 8:

<comment>This example changes a hardcoded live domain's Return-Path and triggers re-verification, with no cleanup. Running it can alter production sending config. Once the domain verifies, the `update` call raises the documented 422 `return_path_unavailable`, so the script cannot be rerun. Create a throwaway domain as `examples/domains.py` does, or read the ID from an environment variable.</comment>

<file context>
@@ -0,0 +1,35 @@
+if not os.environ["RESEND_API_KEY"]:
+    raise EnvironmentError("RESEND_API_KEY is missing")
+
+domain_id = "d91cd9bd-1176-453e-8fc1-35364d380206"
+
+# Enable: activate Forge on an existing domain. Returns the full domain,
</file context>

Comment thread tests/domain_forge_async_test.py Outdated
}
domain = await resend.Domains.Forge.enable_async(params)
assert domain["id"] == "d91cd9bd-1176-453e-8fc1-35364d380206"
assert domain["forge_status"] == "pending"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The async Forge tests only check the returned JSON, so a wrong path or HTTP verb in enable_async, update_async, or verify_async would still pass. Add a self.mock.call_args.kwargs["url"] assertion, as the sync domain_forge_test.py and other async tests do.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/domain_forge_async_test.py, line 23:

<comment>The async Forge tests only check the returned JSON, so a wrong path or HTTP verb in `enable_async`, `update_async`, or `verify_async` would still pass. Add a `self.mock.call_args.kwargs["url"]` assertion, as the sync `domain_forge_test.py` and other async tests do.</comment>

<file context>
@@ -0,0 +1,87 @@
+        }
+        domain = await resend.Domains.Forge.enable_async(params)
+        assert domain["id"] == "d91cd9bd-1176-453e-8fc1-35364d380206"
+        assert domain["forge_status"] == "pending"
+
+    async def test_domain_forge_enable_async_without_return_path(self) -> None:
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

View guided diff | Re-trigger cubic

@lucasfcosta
lucasfcosta marked this pull request as ready for review November 11, 2026 21:24
@lucasfcosta lucasfcosta changed the title feat(domains): add forge activation methods and forge_status feat(domains): add forge activation methods Nov 11, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 7 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

View guided diff | Re-trigger cubic

@lucasfcosta
lucasfcosta force-pushed the feature/dev-2472-python-sdk-internal-squad-devtools branch from 1cbc387 to 240fdd5 Compare November 11, 2026 23:50

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 7 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

View guided diff | Re-trigger cubic

@lucasfcosta lucasfcosta changed the title feat(domains): add forge activation methods feat(domains): add forge activation methods and forge_status Nov 11, 2026
@github-actions github-actions Bot added the linear-synced PR has been synced to Linear label Nov 12, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lucasfcosta
lucasfcosta force-pushed the feature/dev-2472-python-sdk-internal-squad-devtools branch from 240fdd5 to c8a396f Compare November 12, 2026 12:45
@lucasfcosta lucasfcosta changed the title feat(domains): add forge activation methods and forge_status feat(domains): add forge activation methods Nov 12, 2026
Matches the reshaped API: Domains.Forge keeps only enable (return_path
optional), Domains.verify gains an optional scope of ["forge"] that
re-checks only the Forge record, and Domains.update gains
forge_return_path to move the Forge return path without starting
verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and 2 new issues found across 9 files (changes from recent commits).

Confidence score: 4/5

  • In resend/domains/_domains.py, these domain-management methods won't work with sending-only API keys. Confirm production callers use full-access keys.
  • In resend/domains/_domains.py, the Forge return-path docstring says “another host,” which could lead callers to pass a full hostname instead of a subdomain label. Update it to say “subdomain.”
  • In tests/domains_async_test.py, the test could pass even if verify_async posts to the wrong URL. Add the URL assertion used by the sync counterpart.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="resend/domains/_domains.py">

<violation number="1" location="resend/domains/_domains.py:21">
P2: Custom agent: **API Key Permission Check SDK Methods**

Please confirm that production callers use full-access API keys for these new domain-management operations; sending-only keys cannot access non-email endpoints.</violation>

<violation number="2" location="resend/domains/_domains.py:156">
P3: This docstring says the Forge return path moves "to another host", but the value is a subdomain label, as `Domains.Forge.enable` documents for `return_path`. Say "subdomain" so callers don't pass a full hostname.</violation>
</file>

Requires human review: Auto-approval skipped because cubic reviewed only this push, not the earlier force-push. Comment @cubic review to review the whole pull request.

View guided diff | Re-trigger cubic


TlsOptions = Literal["enforced", "opportunistic"]

VerifyDomainScope = Literal["forge"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: API Key Permission Check SDK Methods

Please confirm that production callers use full-access API keys for these new domain-management operations; sending-only keys cannot access non-email endpoints.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At resend/domains/_domains.py, line 21:

<comment>Please confirm that production callers use full-access API keys for these new domain-management operations; sending-only keys cannot access non-email endpoints.</comment>

<file context>
@@ -18,6 +18,8 @@
 
 TlsOptions = Literal["enforced", "opportunistic"]
 
+VerifyDomainScope = Literal["forge"]
+
 
</file context>

"""
forge_return_path: NotRequired[str]
"""
Move the domain's Forge return path to another host. Only possible

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This docstring says the Forge return path moves "to another host", but the value is a subdomain label, as Domains.Forge.enable documents for return_path. Say "subdomain" so callers don't pass a full hostname.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At resend/domains/_domains.py, line 156:

<comment>This docstring says the Forge return path moves "to another host", but the value is a subdomain label, as `Domains.Forge.enable` documents for `return_path`. Say "subdomain" so callers don't pass a full hostname.</comment>

<file context>
@@ -149,6 +151,21 @@ class UpdateParams(TypedDict):
         """
+        forge_return_path: NotRequired[str]
+        """
+        Move the domain's Forge return path to another host. Only possible
+        while the Forge record is still verifying. Moving the path does not
+        start verification: fetch the new record with `Domains.get`, add it to
</file context>
Suggested change
Move the domain's Forge return path to another host. Only possible
Move the domain's Forge return path to another subdomain. Only possible

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

linear-synced PR has been synced to Linear

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant